﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Threading;
using PasswordProvider.ObjectModel.Properties;


namespace PasswordProvider.ObjectModel
{
    /// <summary>
    /// Enumerated type to indicate the method used for invalidating the master password.
    /// </summary>
    [Flags]
    public enum AuthenticationInvalidationMethods
    {
        None,
        InvalidateAfterAccess,
        InvalidateAfterTimeout
    }
   
    /// <summary>
    /// SecurityManager handles all things related to the encryption engine (Cypher). 
    /// The main purpose of SecurityManager is to isolate the active instance of the Cypher and
    /// protect it for security purposes. No other class needs to know about the Cypher class. 
    /// It also serves as a wrapper for Cypher and provides a simplified interface. 
    /// SecurityManager handles authentication invalidation, depending on the value of the
    /// AuthenticationInvalidationMethod setting stored in the application settings. This includes the management
    /// of the timer if the authentication invalidation method is to invalidate after time out.
    /// </summary>
    public class SecurityManager
    {
        public static event EventHandler AuthenticationInvalidatedByTimer;

        #region Fields, Construction
        
        private Cipher _cipher = new Cipher();

        // Using DispatchTimer because it raises it's timer event on the UI thread.
        private DispatcherTimer _invalidationTimer;
        
        /// <summary>
        /// Default constructor
        /// </summary>
        internal SecurityManager(Cipher cipher)
        {
            _cipher = cipher;

            // Add a handler for settings property change.
            Settings.Default.PropertyChanged += new PropertyChangedEventHandler(HandleSettingsChanged);

            if (UsingInvalidationMethod(AuthenticationInvalidationMethods.InvalidateAfterTimeout))
            {
                SetupInvalidationTimer();
            }
        }

        #endregion

        #region Public

        /// <summary>
        /// Returns true when the Cipher is in a state ready to encrypt and decrypt values.
        /// </summary>
        public bool IsAuthenticated
        {
            get { return _cipher != null ? _cipher.IsAuthenticated : false; }
        }
       
        /// <summary>
        /// Attempts to set the master password of the Cipher, return true if the password is correct,
        /// false otherwise.
        /// </summary>
        public bool Authenticate(ProtectedString password)
        {
            bool authenticated = _cipher.Authenticate(password);
            if (authenticated && UsingInvalidationMethod(AuthenticationInvalidationMethods.InvalidateAfterTimeout))
            {
                SetupInvalidationTimer();
            }
            return authenticated;
        }

        /// <summary>
        /// Puts the Cipher into a state requiring the master password for any action to be taken. 
        /// Authenticate must be called before accessing protected data.
        /// </summary>
        public void InvalidateAuthentication()
        {
            _cipher.InvalidateAuthentication();
        }

        /// <summary>
        /// Decrypts the value contained by the EncryptedValue parameter, and returns it,
        /// carefully stored in a ProtectedString. An empty ProtectedString is returned
        /// when the EncryptedValue does not contain a value. If AuthenticationInvalidationMethod
        /// is InvalidateAfterAccess, this will trigger invalidation.
        /// </summary>
        public ProtectedString GetValueDecrypted(EncryptedValue holder)
        {
            return GetValueDecrypted(holder, true);
        }

        /// <summary>
        /// The value is enctyped and stored inside the EncryptedValue holder.
        /// </summary>
        public void SetValueEncrypted(ProtectedString value, EncryptedValue holder)
        {
            holder.SetValueEncrypted(value, _cipher);
        }

        #endregion

        #region Internal 

        /// <summary>
        /// Decrypts the value contained by the EncryptedValue parameter, and returns it,
        /// carefully stored in a ProtectedString. An empty ProtectedString is returned
        /// when the EncryptedValue does not contain a value.
        /// </summary>
        /// <param name="triggerInvalidation">
        /// Allows overriding default behavior when AuthenticationInvalidatinMethod == InvalidateAfterAccess. Ignored
        /// otherwise.
        /// </param>
        internal ProtectedString GetValueDecrypted(EncryptedValue holder, bool triggerInvalidation)
        {
            ProtectedString p = holder.GetValueDecrypted(_cipher);
            if (p == null)
                p = new ProtectedString();
            else if (triggerInvalidation && UsingInvalidationMethod(AuthenticationInvalidationMethods.InvalidateAfterAccess))
                InvalidateAuthentication();

            return p;
        }

        #endregion // Internal

        #region Private Methods


        /// <summary>
        /// Set up the timer which will call InvalidateAuthentication after an
        /// interval defined in setting ValidationTimeoutMinutes
        /// </summary>
        private void SetupInvalidationTimer()
        {
            
            // Create new timer if necessary.
            if (_invalidationTimer == null)
            {
                _invalidationTimer = new DispatcherTimer();
                
                _invalidationTimer.Tick += delegate 
                {                   
                    // Note: Timer will be reset the next time Authenticate is called.
                    _invalidationTimer.Stop();
                    // Clear the authentication data and raise event.
                    _cipher.InvalidateAuthentication();
                    // Raise event.
                    AuthenticationInvalidatedByTimer(this, new EventArgs());                 
                };
            }
            // set up the interval in min and seconds.
            int minutes = (int)Math.Truncate(Settings.Default.ValidationTimeoutMinutes);
            int seconds = (int)((Settings.Default.ValidationTimeoutMinutes - minutes) * 60);

            _invalidationTimer.Interval = new TimeSpan(0,minutes, seconds );
            _invalidationTimer.Start();
            
        }

        // Event handler for when a setting is changed. 
        void HandleSettingsChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "AuthenticationInvalidationMethod")
            {              
                // Need to respond for timeout, setup the timer.
                if (UsingInvalidationMethod(AuthenticationInvalidationMethods.InvalidateAfterTimeout))                       
                {
                    SetupInvalidationTimer();
                }
                // Kill the timer if not using.
                else if (_invalidationTimer != null)
                {
                    _invalidationTimer.Stop();
                    _invalidationTimer = null;
                }

            }
            else if (e.PropertyName == "ValidationTimeoutMinutes")
            {
                if (UsingInvalidationMethod(AuthenticationInvalidationMethods.InvalidateAfterTimeout))
                {
                    SetupInvalidationTimer();
                }
            }
        }

        // Return true if the stored value in settings for AuthenticationInvalidationMethod
        // indicates that parameter method is in use. Useful because of flags simulation
        // in the definition of the values in the AuthenticationInvalidationMethod enumeration.
        private static bool UsingInvalidationMethod(AuthenticationInvalidationMethods method)
        {
            return (method & Settings.Default.AuthenticationInvalidationMethod) == method;
        }

        #endregion

    }
}
